Skip to content

feat: shared model preferences across web and mobile#4388

Open
iscekic wants to merge 5 commits into
mainfrom
shared-model-preferences
Open

feat: shared model preferences across web and mobile#4388
iscekic wants to merge 5 commits into
mainfrom
shared-model-preferences

Conversation

@iscekic

@iscekic iscekic commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Introduces a user-scoped user_model_preferences table that persists model favorites and last-selected (model + optional variant) per signed-in user. A new tRPC modelPreferences router exposes get, setLastSelected, clearLastSelected, addFavorite, removeFavorite, and setFavorites. The get procedure requires the caller to be a member of the given organization (via ensureOrganizationAccess, matching organizationMemberProcedure semantics) and intersects results with that org's available-model policy so disallowed models are silently dropped, matching the existing org default_model semantics in apps/web/src/routers/organizations/organization-settings-router.ts. addFavorite/removeFavorite are atomic jsonb upserts, so concurrent toggles from two devices don't lose updates.

Architectural changes:

  • New user_model_preferences table (packages/db/src/schema.ts, generated migration 0176_flowery_omega_flight.sql) with onDelete: 'cascade', onUpdate: 'cascade' to kilocode_users.id so user deletion cleans up the row.
  • New tRPC router at apps/web/src/routers/model-preferences-router.ts registered in root-router.ts.
  • Web: NewSessionPanel.tsx prefers the server's lastSelected (model and variant) for the initial model seed, falls back to localStorage, then org default, then first available. Picks and variant changes write through to the server in addition to localStorage.
  • Mobile: new useModelPreferences and useAutoSelectModel hooks layer the server value over the local cache (feat(mobile): polish agent chat (persist model, attachments, copy, reasoning default) #4387's per-context usePersistedAgentModel store). The agent-chat model-picker shows a Favorites group with a 44dp star toggle per row (no star on the pinned CLI pseudo-model row from fix(remote): preserve CLI model for remote sessions #4383, which stays above FAVORITES). new.tsx hydrates from server lastSelected → local cache → org default (same /defaults endpoint web uses) → first available, matching web's getPreferredInitialModel precedence. Mid-session model changes also write through to the server preference.

The deprecated kilocode_users.default_model column and the existing web localStorage getLastUsedModel/setLastUsedModel are intentionally left untouched (still referenced by softDeleteUser and as a local cache, respectively).

Extension and CLI live in a separate repo (~/Projects/kilocode) and are out of scope for this PR. The tRPC router is the stable contract they will consume. Tracked as a follow-up.

Verification

Manual verification: not yet run on a live dev session. The work was verified by:

  • pnpm test:db then pnpm drizzle migrate applied the migration cleanly; psql\d user_model_preferences showed the expected shape, indexes, and FK.
  • pnpm --filter web test -- model-preferences-router — 14/14 router tests pass (including cascade-on-user-delete, non-member UNAUTHORIZED, org intersection with a real org, and concurrent-add atomicity).
  • pnpm --filter web test — full web test run, 8075 tests pass, 3 skipped.
  • pnpm --filter kilo-app test — 380/380 mobile tests pass (includes the CLI-row + favorites interaction tests added while merging main).
  • scripts/typecheck-all.sh --changes-only — clean.
  • pnpm --filter web lint, pnpm --filter kilo-app lint, pnpm --filter @kilocode/db lint — all 0 errors.
  • pnpm format — formatted the touched files; full repo pnpm format applied to all 5932 files.

Follow-ups: live in-browser smoke test of (a) favoriting a model on web and seeing it on mobile after a sign-in, (b) the inverse, and (c) verifying the org intersection on a non-enterprise org. Skipped here to keep the PR reviewable; recommend doing both in the extension/CLI follow-up PR.

Visual Changes

N/A

Reviewer Notes

  • apps/web/src/lib/hooks/use-model-preferences.ts exposes only what web consumes today (lastSelected + setLastSelected); the favorites surface (with optimistic updates) lives in the mobile hook and can be added to web when it grows a favorites UI.
  • useAutoSelectModel waits for the server preferences query (and the SecureStore cache) before locking in a selection, so the server lastSelected reliably wins on cold start; on error/offline the query settles and the local fallback applies. Mutations invalidate modelPreferences.get with a partial key so the org-scoped and org-less caches stay in sync.
  • buildModelPickerRows signature changed to require favoriteIds: Set<string>; the existing test was updated to match.
  • The local cache layer is feat(mobile): polish agent chat (persist model, attachments, copy, reasoning default) #4387's usePersistedAgentModel (per-context agent-model-preference store), adopted during the merge of main; this branch's earlier single-entry SecureStore hook was dropped in its favor. Model changes inside an existing session also write through to the server preference now.
  • Decision 6 was scoped mid-PR to "web + mobile only" per the user's revision; the decision record reflects this.

Introduce a user-scoped user_model_preferences table that persists model
favorites and last-selected (model + optional variant) per signed-in user.
A new tRPC model-preferences router exposes get/setLastSelected/
clearLastSelected/addFavorite/removeFavorite/setFavorites; get intersects
results with the active org's available-model policy so disallowed models
are silently dropped.

Web: NewSessionPanel reads the server's lastSelected to seed the initial
model and writes back on every model or variant change. The existing
localStorage cache remains as a fallback.

Mobile: new useModelPreferences + usePersistedAgentModel hooks layer the
server value over a SecureStore cache. The agent-chat model picker shows
a Favorites group with a 44dp star toggle per row, and new.tsx hydrates
from server lastSelected then falls back to the local cache.

Extension and CLI live in a separate repo and are tracked as a follow-up;
the tRPC router is the stable contract they will consume.

Decision record: .plans/shared-model-preferences-decisions.md.
@iscekic iscekic self-assigned this Jul 3, 2026
Comment thread apps/web/src/routers/model-preferences-router.ts
Comment thread apps/web/src/routers/model-preferences-router.ts
Comment thread apps/web/src/routers/model-preferences-router.ts
Comment thread apps/web/src/routers/model-preferences-router.ts Outdated
@kilo-code-bot

kilo-code-bot Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

All 4 previously reported issues (missing org-membership check in get, non-atomic favorite add/remove, unreachable input fallback) are fixed in the latest commit, and no new issues were introduced by the incremental changes.

Files Reviewed (8 files)
  • apps/mobile/src/app/(app)/agent-chat/new.tsx
  • apps/mobile/src/lib/hooks/use-auto-select-model.ts
  • apps/mobile/src/lib/hooks/use-available-models.ts
  • apps/mobile/src/lib/hooks/use-model-preferences.ts
  • apps/web/src/components/cloud-agent-next/NewSessionPanel.tsx
  • apps/web/src/lib/hooks/use-model-preferences.ts
  • apps/web/src/routers/model-preferences-router.test.ts
  • apps/web/src/routers/model-preferences-router.ts
Previous Review Summary (commit 618d2c6)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 618d2c6)

Status: 4 Issues Found | Recommendation: Address before merge

Executive Summary

modelPreferences.get accepts a client-supplied organizationId with no membership check, letting any authenticated user probe another org's model-restriction policy through getAvailableModelsForOrganization.

Overview

Severity Count
CRITICAL 1
WARNING 2
SUGGESTION 1
Issue Details (click to expand)

CRITICAL

File Line Issue
apps/web/src/routers/model-preferences-router.ts 48 get uses a client-supplied organizationId without verifying org membership, unlike other consumers of getAvailableModelsForOrganization

WARNING

File Line Issue
apps/web/src/routers/model-preferences-router.ts 90 addFavorite read-modify-write on favorites is not atomic; concurrent calls can lose updates
apps/web/src/routers/model-preferences-router.ts 109 removeFavorite has the same non-atomic lost-update race as addFavorite

SUGGESTION

File Line Issue
apps/web/src/routers/model-preferences-router.ts 47 getInput ?? z.object({}).optional() fallback is unreachable dead code
Files Reviewed (16 files)
  • apps/mobile/src/app/(app)/agent-chat/model-picker.tsx
  • apps/mobile/src/app/(app)/agent-chat/new.tsx
  • apps/mobile/src/lib/hooks/use-auto-select-model.ts
  • apps/mobile/src/lib/hooks/use-model-preferences.ts
  • apps/mobile/src/lib/hooks/use-persisted-agent-model.ts
  • apps/mobile/src/lib/model-picker-rows.test.ts
  • apps/mobile/src/lib/model-picker-rows.ts
  • apps/mobile/src/lib/storage-keys.ts
  • apps/web/src/components/cloud-agent-next/NewSessionPanel.tsx - 0 issues
  • apps/web/src/lib/hooks/use-model-preferences.ts - 0 issues
  • apps/web/src/routers/model-preferences-router.test.ts - 0 issues
  • apps/web/src/routers/model-preferences-router.ts - 4 issues
  • apps/web/src/routers/root-router.ts - 0 issues
  • packages/db/src/migrations/0176_flowery_omega_flight.sql - 0 issues
  • packages/db/src/migrations/meta/* - generated, skipped
  • packages/db/src/schema.ts - 0 issues

Fix these issues in Kilo Cloud


Reviewed by claude-sonnet-5-20260630 · Input: 38 · Output: 9.2K · Cached: 1.1M

Review guidance: REVIEW.md from base branch main

iscekic added 4 commits July 3, 2026 18:14
- Require org membership in modelPreferences.get before resolving an
  arbitrary organizationId (matches organizationMemberProcedure semantics)
- Make addFavorite/removeFavorite atomic jsonb upserts so concurrent
  toggles from two devices no longer lose updates; cover with a test
- Wait for the server preferences query in useAutoSelectModel so the
  shared lastSelected is not lost to a race against SecureStore on cold
  start; honor the server variant on web seeding too
- Share one usePersistedAgentModel instance between new.tsx and the
  auto-select hook
- Invalidate modelPreferences.get with a partial key so org-scoped and
  org-less caches stay in sync after mutations
- Derive the query data type from the router output, drop the hand-copied
  LastSelected type and cast, trim the web hook to its used surface,
  dedupe the optimistic favorites logic, parallelize the get() reads
Mobile now mirrors web's getPreferredInitialModel precedence: server
lastSelected -> local cache -> org default (/api/organizations/:id/defaults,
same endpoint web uses) -> first available. Personal sessions are unchanged
(no defaults fetch is made without an organizationId).
Composes #4383's pinned CLI model row with the favorites picker rows:
the CLI row stays first (above FAVORITES), is excluded from the favorites
and section groups, and does not render a favorite star.
Adopts #4387's per-context local model persistence (agent-model-preference
store + usePersistedAgentModel) as the local cache layer, replacing this
branch's simpler single-entry SecureStore hook and its storage key.
useAutoSelectModel now resolves the local fallback via
resolveModelForContext; the server lastSelected -> local -> org default ->
first-available precedence is unchanged. Model changes in an existing
session (session-detail-content) now also write through to the server
preference, mirroring the new-session screen.
@iscekic iscekic requested a review from jeanduplessis July 3, 2026 17:47
@iscekic iscekic enabled auto-merge (squash) July 3, 2026 18:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant